package com.pubinfo.passbook.engine.factory.impl;

import com.pubinfo.passbook.common.exception.ServiceException;
import com.pubinfo.passbook.common.model.http.engine.ComputeChainParam;
import com.pubinfo.passbook.common.service.engine.EngineComputeChainService;
import com.pubinfo.passbook.engine.chain.common.ComputeCommonNumberChain;
import com.pubinfo.passbook.engine.utils.ValidateUtils;
import com.pubinfo.passbook.engine.cell.ComputeUnit;
import com.pubinfo.passbook.engine.chain.ComputeChain;
import com.pubinfo.passbook.engine.chain.medicine.ComputeMedicineChain;
import com.pubinfo.passbook.engine.chain.medicine.ComputeMedicinePayinforamtionChain;
import com.pubinfo.passbook.common.entity.TEngineComputeChain;
import com.pubinfo.passbook.engine.factory.ComputeChainFactory;
import com.pubinfo.passbook.engine.constants.ComputeChainType;
import com.pubinfo.passbook.engine.factory.ComputeUnitFactory;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;


/**
 * 默认的计算链工厂
 *
 * @author zhouch
 * @date 2021/12/13 14:23
 */
@Component
@Slf4j
public class DefaultComputeChainFactory implements ComputeChainFactory {

    /**
     * 为了避免计算链出现循环引用的情况，这里使用thread-local来保存当前已经初始化的计算链ID，
     * 如果出现重复ID，则说明计算链已经出现循环，需要立即终止。
     */
    private static final ThreadLocal<List<Integer>> initChainIds = new ThreadLocal<>();

    @Resource
    EngineComputeChainService chainService;

    @Resource
    ComputeUnitFactory unitFactory;

    @Override
    public ComputeChain<?, ?> getComputor(@NonNull Integer chainId) {

        // 获得计算链元数据
        TEngineComputeChain chainInfo = chainService.getById(chainId);

        if (chainInfo == null) {
            throw new ServiceException("不存在该计算链信息. ID: " + chainId);
        }

        return getComputor(chainInfo);
    }

    @Override
    public ComputeChain<?, ?> getComputor(@NonNull TEngineComputeChain chainInfo) {

        log.info("计算链信息: 名称: {}, 类型: {}, 描述: {}", chainInfo.getName(), chainInfo.getType(), chainInfo.getDescription());

        if (chainInfo.getId() != null) {
            // 如果存在ID, 则检查计算链是否存在循环引用
            addAndCheckCycleChainReference(chainInfo.getId());
        }

        ComputeChain<?, ?> computor = getComputor(chainInfo.getBody(), chainInfo.getType());

        if (chainInfo.getId() != null) {
            removeCycleChainReference(chainInfo.getId());
        }

        return computor;
    }

    /**
     * 如果是想初始化在计算链表中的计算链,请勿直接直接调用该方法. 请使用上面的TEngineComputeChain参数的重载
     * <p>
     * (⚠ 该方法无法检查计算链中的循环引用,
     * 如果你非常不幸的使用了该方法来初始化计算链表中的计算链,
     * 又恰巧用户错误的配置的一个循环引用的计算链,
     * Then -> **系统会崩溃**!!)
     * <p>
     * 实际上通常不需要担心这种可能性, 计算链的循环引用一般会在保存的时候被引擎检查出来.
     *
     * @param params
     * @param targetChainType
     * @return
     */
    @Override
    public ComputeChain<?, ?> getComputor(Collection<ComputeChainParam> params, String targetChainType) {
        ComputeChain chain = getComputeChainByType(targetChainType);

        log.info("初始化计算链信息: 计算链长度: {} ,计算链类型: {}", params.size(), targetChainType);

        /**
         * 逐个从ComputeUnitFactory工厂中初始化各个计算单元。
         */
        List<? extends ComputeUnit> allUnits =
                params.stream().map(it -> unitFactory.getComputor(it, targetChainType)).collect(Collectors.toList());

        checkComputeUnitListType(allUnits, targetChainType, chain.getResultType());

        /**
         * 装载
         */
        chain.loadUnits(allUnits);

        return chain;
    }


    /**
     * 通过计算链类型来获得对应的计算链实例
     *
     * @param chainType
     * @return
     */
    private ComputeChain<?, ?> getComputeChainByType(String chainType) {
        switch (chainType) {
            case ComputeChainType.MEDICINE:
                return new ComputeMedicineChain();
            case ComputeChainType.MEDICINE_SUB_LOOP:
                return new ComputeMedicinePayinforamtionChain();
            case ComputeChainType.COMMON:
                return new ComputeCommonNumberChain<>();
            default:
                throw new ServiceException("未知的计算链类型.");
        }
    }


    /**
     * 最终检查计算单元的链类型和结果类型是否匹配
     *
     * @param units
     * @param targetChainType
     * @param resultType
     */
    private void checkComputeUnitListType(Collection<? extends ComputeUnit> units, String targetChainType, String resultType) {
        units.forEach(t -> {
            if (false == ValidateUtils.checkChainType(t, targetChainType)) {
                throw new ServiceException("计算链类型不匹配: 原类型: " + t.getChainType() + " 目标类型: " + targetChainType);
            }

            if (false == ValidateUtils.checkResultType(t, resultType)) {
                throw new ServiceException("计算链的结果类型不匹配: 原类型: " + t.getResultType() + " 目标类型: " + resultType);
            }
        });
    }

    /**
     * 检查计算链循环引用
     */
    private void addAndCheckCycleChainReference(Integer chainId) {

        if (initChainIds.get() == null || initChainIds.get().size() == 0) {
            initChainIds.set(new ArrayList<>());
        }

        List<Integer> chainIds = initChainIds.get();

        /**
         * 是否已经初始化过了，
         * 通常，如果一个计算链内部出现了同一个计算链初始化2次(也就是chainIds出现了2个重复的计算链ID)，必然是发生了循环引用的情况
         */
        if (chainIds.contains(chainId)) {
            throw new ServiceException("发生计算链循环引用，请仔细检查, 循环的计算链ID: " + chainId);
        } else {
            chainIds.add(chainId);
        }

        initChainIds.set(chainIds);
    }

    /**
     * 移除已经初始化完成的计算链ID
     */
    private void removeCycleChainReference(Integer chainId) {
        List<Integer> chainIds = initChainIds.get();
        chainIds.removeIf(
                t -> t.equals(chainId)
        );

        initChainIds.set(chainIds);

        /**
         * chainIds为空说明全部计算链已经初始化完毕，释放ThreadLocal，避免内存泄漏
         */
        if (chainIds.isEmpty()) {
            initChainIds.remove();
        }
    }

}
